1 Objetivos

En esta práctica vamos a hacer varios ejercicios orientados a afianzar nuestro conocimiento sobre la programación de sistemas en C y el uso de su biblioteca estándar para operaciones básicas sobre cadenas de caracteres, entrada salida y ficheros.

Se aconseja al alumno que cree un directorio para la práctica con un subdirectorio por ejercicio. En las instrucciones se asume que el ejercicio N se hace en un subdirectorio llamado ejercicioN dentro del directorio común para la práctica.

El archivo ficheros_p2.tar.gz contiene una serie de ficheros que pueden usarse como punto de partida para el desarrollo de los ejercicios de esta práctica, así como unos makefiles que pueden ser usados para la compilación de los distintos proyectos.

2 Ejercicios

Ejercicio 1: Manejo básico de ficheros con librería estándar

Analiza el código del programa show_file.c, que lee byte a byte el contenido de un fichero, cuyo nombre se pasa como parámetro, y lo muestra por pantalla usando funciones de la biblioteca estándar de “C”. Compila y comprueba el funcionamiento correcto del programa. Después modifica el código reemplazando el uso de getc() por el de la función fread() y el uso de putc() por el de la función fwrite(). Consulta las páginas de manual correspondientes.

Ejercicio 2: Escritura y lectura de cadenas de caracteres en ficheros

Desarrollar dos programas sencillos write_strings.c y read_strings.c que permitan respectivamente escribir y leer de un fichero un conjunto de cadenas de caracteres de longitud variable terminadas por '\0'. Dicho caracter terminador deberá almacenarse en el fichero con el resto de caracteres de cada cadena. Para el desarrollo de los dos programas se utilizarán las siguientes funciones de la biblioteca estándar: fopen(),fclose(), fread(), fwrite(), fseek() y malloc()

El programa write-strings.c aceptará como primer parámetro el nombre de un fichero de texto donde se escribirán los strings pasados a continuación a la línea de comandos (argumento 2, argumento 3, etc.). Si el fichero destino existe, el programa reescribirá su contenido.

El programa read-strings.c aceptará como parámetro el nombre del fichero de texto donde se almacenen las cadenas de caracteres terminadas en '\0'. Este programa leerá las cadenas y las imprimirá por pantalla separadas por un salto de línea, como se muestra en el siguiente ejemplo de ejecución:

Por simplicidad para la implementación del programa read-strings.c, se ha de desarrollar una función auxiliar char* loadstr(FILE* input). Esta función lee una cadena de caracteres terminada en '\0' del fichero cuyo descriptor se pasa como parámetro, reservando dinámicamente la cantidad de memoria adecuada para la cadena leída y retornando dicha cadena. La función tendrá que averiguar primero el número de caracteres de la cadena que comienza a partir de la ubicación actual del puntero de posición del fichero, leyendo caracter a caracter. Una vez detectado el caracter terminador, restaurará el indicador de posición del fichero (moviéndolo hacia atrás) y, finalmente realizará una lectura de la cadena completa.

Ejercicio 3: Gestión de ficheros de texto y binarios con la biblioteca estándar de C

En este ejercicio de la práctica se desarrollará un programa C más elaborado que lea y escriba de ficheros regulares tanto de texto, como en formato binario. Para su implementación, los estudiantes deberán utilizar al menos las siguientes funciones de la biblioteca estándar de C: getopt, printf, fopen, fclose, fgets, fscanf, feof, fprintf, fread, fwrite y strsep. Se deben consultar las páginas de manual de estas funciones en caso de duda sobre su comportamiento.

El ejercicio consta de 3 partes (más extensiones opcionales), donde se irán implementando gradualmente distintas características del programa:

Parte A

Desarrollar un programa student-records.c que lea un fichero de texto con información de distintos estudiantes, e imprima por la salida estándar la información leida en un formato amigable. El fichero de texto de estudiantes almacena un registro de 4 campos por estudiante (identificador numérico único, NIF, nombre, y apellido). Para simplificar el parsing del fichero, su primera línea contiene el número de registros de estudiantes, y a continuación se encuentran los distintos registros de estudiante, uno por línea, con campos separados por “:”, como en el siguiente ejemplo:

El fichero de ejemplo almacena 4 registros de estudiantes, donde la información del primer estudiante es la siguiente:

  • Identificador numérico: 27
  • NIF: 67659034X
  • Nombre: Chris
  • Apellido: Rock

Para leer un fichero de texto de estudiantes e imprimir su contenido en formato amigable, el programa student-records deberá invocarse especificando las opciones -i (input) y -p (print) simultáneamente en la línea de comando, donde la opción -i acepta un parámetro indicando la ruta del fichero de texto. Así por ejemplo, asumiendo que existe un fichero students-db.txt en el directorio actual que contiene el texto de ejemplo mostrado anteriormente, la ejecución del programa producirá la siguiente salida:

Por simplicidad en la implementación, cada registro se imprimirá por la salida estándar tan pronto como se procese la línea de texto del fichero de entrada correspondiente a dicho registro. De este modo no será necesario almacenar en memoria la información completa del fichero. Para representar en memoria de los distintos campos del registro, se recomienda el uso de la estructura student_t, definida en el fichero de cabecera defs.h proporcionado con la práctica.

Además de las opciones arriba mencionadas, se implementará una opción -h (help), que imprima el listado de opciones soportadas por el programa:

La salida generada por esta opción deberá modificarse a medida que se implementen opciones adicionales en el programa, correspondientes a los distintos apartados.

Parte B

Extender la funcionalidad del programa student-records implementando una nueva opción-o. Esta opción permitirá generar una representación binaria de los registros de estudiantes, y volcarla en un fichero de salida cuya ruta se pasará como parámetro a la opción -o. Al leer cada entrada del fichero de texto, el programa almacenará la información del estudiante en un registro representado mediante el siguiente tipo de datos:

El fichero binario a generar tendrá la siguiente estructura. Los primeros 4 bytes del fichero (int) almacenarán el número de registros de estudiantes. A continuación se escribirán los datos de cada registro de estudiantes, uno detrás del otro, almacenando por cada uno de ellos su ID de estudiante (entero de 4 bytes), su NIF, su nombre y apellido (en este orden). Para todas las cadenas de caracteres a escribir en el fichero se almacenará también el caracter terminador, lo cual es clave para poder leer el fichero a posteriori (parte C del ejercicio).

En el siguiente ejemplo de ejecución se hace uso del comando xxd para mostrar el contenido del fichero generado (nótese que este fichero no puede imprimirse satisfactoriamente con cat, al tratarse de un fichero binario):

Parte C

Implementar una nueva opción -b (binary ) en el programa que permita imprimir el contenido de un fichero binario de estudiantes existente usando el mismo formato de salida que el especificado en la Parte A del ejercicio. Al indicar la opción -b (en lugar de -p) en la línea de comandos, el programa asumirá que el formato del fichero de entrada será binario en lugar de texto.

El siguiente ejemplo ilustra la funcionalidad que ha de implementarse en este apartado:

Pista: Se aconseja reutilizar para la implementación la función loadstr(), desarrollada en el ejercicio 2.

Partes opcionales

Opcional 1

En los apartados obligatorios de la práctica se asume que el programa procesa los registros leidos del fichero de entrada uno a uno, o bien escribiendolos en la salida estándar (opciones -p y -b ) o volcando cada registro leído en el fichero de salida (opción -o) según se lee. En esta parte opcional se propone refactorizar el programa del código student_records.c de tal forma que los registros del fichero de entrada –ya esté en formato binario o texto– se lean todos de forma consecutiva y se almacenen en un vector de registros de tipo student_t. Para ello se han de definir 2 funciones auxiliares:

  • student_t* read_student_text_file(FILE* students, int* nr_entries)

    Esta función lee toda la información de un fichero de texto de registros de estudiantes ya abierto, y devuelve tanto el número de registros en el fichero (parámetro de retorno nr_entries), como el array de registros de estudiantes (valor de retorno de la función). La memoria del array que se retorna debe reservarse con malloc() dentro de la propia función.

  • student_t* read_student_binary_file(FILE* students, int* nr_entries)

    Igual que la función read_student_text_file() pero leyendo un fichero binario de registros de estudiantes

Para completar esta parte opcional, se ha de reescribir el resto de funciones implementadas para usar la nueva funcionalidad proporcionada por estas funciones. Así por ejemplo, la función que antes leía del fichero de texto de entrada, e imprimía uno a uno los registros en la salida estándar, debe ahora leer todos los registros de golpe usando read_student_text_file(), y a continuación imprimir los registros almacenados en el vector devuelto usando un bucle.

Opcional 2

Extender la funcionalidad del programa student_records.c con una nueva opción -a que permita añadir registros extra de estudiantes al final de un fichero existente, ya sea en formato binario o texto. Los nuevos registros a añadir se especificarán en modo texto en la línea de comando, con campos separados por :, y donde los registros se separarán por espacios. El programa inferirá automáticamente el formato del fichero existente en base a su extensión (“.txt”: texto; “.bin”: binario).

Ejemplo de ejecución:

Nótese que en este caso la opción -a no acepta ningún argumento, sino que los registros se proporcionan como argumentos extra en la línea de comandos (sin opción asociada). Para procesar estos argumentos, ha de emplearse la variable global optind de getopt(), que al finalizar el procesamiento de opciones almacena el índice del primer argumento extra proporcionado. En el ejemplo de comando mostrado anteriormente, optind valdrá 4 al acabar el bucle de procesamiento de opciones, ya que el primer argumento extra constituye el token número 4 de la línea de comandos. De este modo la expresión de C &argv[optind] puede emplearse para acceder al vector de argumentos extra, teniendo el siguiente contenido en el ejemplo: {"23:43159076B:Michael:Jordan", "30:34651129G:Stephen:Curry", NULL}